type NumCNOptions = {
  useLiang?: boolean;        // 是否用 “两” 替代部分 “二”（两百/两千/两万/两亿/两兆）；默认 true
  formal?: boolean;          // 财务大写（壹贰叁… 拾佰仟）；默认 false
  traditional?: boolean;     // 繁体单位（萬、億）；默认 false（用 万、亿）
  omitOneBeforeTen?: boolean;// 10~19 是否省略“一”（十/十一…）；默认 true
};

export function numberToChinese(
  input: number | bigint | string,
  opts: NumCNOptions = {}
): string {
  const {
    useLiang = true,
    formal = false,
    traditional = false,
    omitOneBeforeTen = true,
  } = opts;

  if (typeof input === 'number') {
    if (!Number.isFinite(input)) throw new Error('输入不是有限数字');
  }

  const digits = formal
    ? ['零','壹','贰','叁','肆','伍','陆','柒','捌','玖']
    : ['零','一','二','三','四','五','六','七','八','九'];

  const unitsSmall = formal
    ? ['仟','佰','拾',''] : ['千','百','十',''];

  const bigUnits = (() => {
    if (formal && traditional) return ['', '萬', '億', '兆'];
    if (formal && !traditional) return ['', '万', '亿', '兆'];
    if (!formal && traditional) return ['', '萬', '億', '兆'];
    return ['', '万', '亿', '兆'];
  })();

  const omitOneChar = formal ? '壹' : '一';

  // 统一成字符串处理（支持很大的数）
  let s = String(input).trim();
  if (s.length === 0) throw new Error('空输入');

  // 处理符号
  let sign = '';
  if (s[0] === '+' || s[0] === '-') {
    if (s[0] === '-') sign = formal ? '负' : '负';
    s = s.slice(1);
  }

  // 去掉逗号分隔
  s = s.replace(/,/g, '');

  // 处理小数
  const [intPartRaw, fracPartRaw] = s.split('.');
  const intPart = intPartRaw.replace(/^0+(?=\d)/, ''); // 去前导零，但保留单个零
  const fracPart = fracPartRaw ?? '';

  const isAllZeroInt = intPart.replace(/0/g, '') === '';

  const toFrac = (frac: string) => {
    if (!frac) return '';
    const pointChar = '点'; // 财务里也常用“点”
    const fracCN = [...frac].map(ch => {
      if (ch < '0' || ch > '9') throw new Error('小数部分含非数字');
      return digits[+ch];
    }).join('');
    return pointChar + fracCN;
  };

  if (isAllZeroInt) {
    const head = digits[0]; // “零”
    const tail = toFrac(fracPart);
    return sign + (tail ? head + tail : head);
  }

  // 把整数部分每四位一组，右向左
  const groups: string[] = [];
  {
    let t = intPart;
    while (t.length > 4) {
      groups.unshift(t.slice(-4));
      t = t.slice(0, -4);
    }
    groups.unshift(t);
  }

  // 将四位组转中文
  function groupToCN(group: string, isTopGroup: boolean): string {
    // 补齐到4位，便于按位判断
    const g = group.padStart(4, '0');
    const ds = g.split('').map(c => +c); // [千,百,十,个]
    let out = '';
    let pendingZero = false;

    for (let i = 0; i < 4; i++) {
      const d = ds[i];
      const unit = unitsSmall[i];

      if (d === 0) {
        // 组内零：等到下一个非零才补“零”
        if (out !== '') pendingZero = true;
        continue;
      }

      if (pendingZero) {
        out += digits[0];
        pendingZero = false;
      }

      // 处理“十”的省略“一”
      if (i === 2 /*十位*/) {
        if (d === 1 && isTopGroup && out === '' && omitOneBeforeTen) {
          // 省略“一”
          out += unit; // “十”
          continue;
        }
        // 2~9 的十位正常念，不用“两十”
        out += digits[d] + unit;
        continue;
      }

      // 千位/百位可用“两”
      if (!formal && useLiang && d === 2 && (i === 0 || i === 1)) {
        out += '两' + unit;
      } else {
        out += digits[d] + unit;
      }
    }

    // 若整组等于“2”，用于接大单位（如 两万/两亿）
    if (!formal && useLiang && out === digits[2]) {
      out = '两';
    }

    return out;
  }

  // 组装所有组并插入大单位与必要的“零”
  let result = '';
  for (let i = 0; i < groups.length; i++) {
    const val = +groups[i]; // 该组的数值（0~9999）
    const isTop = (result === '');
    if (val === 0) {
      // 跳过，但记住是否需要在将来补“零”（跨 1+ 个零组时由下一条规则处理）
      continue;
    }

    // 如果前面已有内容，且当前组 < 1000（说明有跨位缺口），需要补一个“零”
    // 例：1_0010 => “一万零一十”
    if (result !== '' && val < 1000) {
      // 但要确保上一个非零组不是紧挨着的“零”结尾，这里用规则直接补一次即可
      if (!result.endsWith(digits[0])) {
        result += digits[0];
      }
    }

    // 转该组中文并加上对应大单位
    let seg = groupToCN(groups[i], isTop);
    result += seg + (bigUnits[groups.length - 1 - i] || '');
  }

  // 处理尾部：去掉可能重复的“零”
  result = result.replace(/零+/g, '零').replace(/零$/g, '');

  // 加小数
  const fracCN = toFrac(fracPart);

  return sign + result + fracCN;
}
